Repository navigation
feat: list media via public API and MCP mediaListTool - #1926
Conversation
Adds GET /public/v1/media?page=&search= and a read-only mediaListTool so agents can reuse already-uploaded media paths as attachments instead of re-uploading. getMedia select now also returns type, fileSize, createdAt. Tested: unauthenticated request returns 401; with an API key pagination (pages count, page 2), filename search and the new fields verified; mediaListTool appears in the MCP tools list, returns the same rows, and a draft scheduled through the MCP with a listed path stores that path as the post image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strix Security ReviewNo security issues found. Review summaryReviewed the media-listing feature end to end: the new Updated for Reviewed by Strix |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| @Get('/media') | ||
| getMedia( | ||
| @GetOrgFromRequest() org: Organization, | ||
| @Query('page') page: number, |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Partly confirmed: a non-numeric value or 0 fell back to page 1, but a negative value did produce a negative skip and a 500. Fixed in 063ee50 with a GetMediaDto (page: IsNumber, Min(1), parseInt transform, default 1; search: IsString), same pattern as GetNotificationsDto. Verified: page=abc, 0 and -3 now return 400, page=1.5 parses to 1, normal requests unchanged.
A negative page produced a negative Prisma skip and a 500. GetMediaDto (same shape as GetNotificationsDto) now rejects non-numeric or < 1 values with 400 and defaults to 1. Tested: page=abc / 0 / -3 return 400 with validation messages, page=1.5 parses to 1, no page / page=2 / search unchanged, unauthenticated still 401. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # apps/backend/src/public-api/routes/v1/public.integrations.controller.ts # libraries/nestjs-libraries/src/chat/tools/tool.list.ts
Both columns are never written on upload (every row keeps the schema defaults 'image' and 0, so videos report 'image' and fileSize is 0 for almost every row), which would mislead API and MCP callers. getMedia now only adds createdAt on top of the existing select. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
alt, thumbnail and thumbnailTimestamp are the values last saved from some post's media settings dialog (the post keeps its own copy), so they do not describe the file. The public route and mediaListTool now return only id, name, originalName, path and createdAt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
| .string() | ||
| .optional() | ||
| .describe('Filter by original filename (case-insensitive contains)'), | ||
| page: z.number().optional().describe('Page number, starting at 1'), |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Confirmed: a negative page passed the schema and reached Prisma as a negative skip, which throws. Fixed in dda966e by requiring an integer >= 1 on the tool's page input, matching GetMediaDto on the REST route.
A negative page passed the zod schema, survived the `|| 1` fallback and reached Prisma as a negative skip, which throws. The tool now requires an integer >= 1, like GetMediaDto does for the REST route. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What kind of change does this PR introduce?
Feature (public API + MCP/agent tools, backend). Adds
GET /public/v1/media?page=&search=(api-key guarded,public_api-requestSentry metric like the other routes,pagevalidated by the newGetMediaDto) that returns{ pages, results }from the existingMediaService.getMedia(org-scoped, non-deleted, notprocessing, 18 per page, newest first, case-insensitiveoriginalNamesearch), with each row mapped in the controller toid,name,originalName,pathandcreatedAt. Adds the read-only MCP/agent toolmediaListToolwith the same input and a{ pages, output }result of the same five fields, registered intoolList, plus one system-prompt line telling the agent to reuse listed paths before asking for a URL or re-uploading.getMedia's select additionally returnscreatedAt(additive; the UI media grid uses the same method and still gets its existing fields). No schema change, no migration, no env vars; existing routes and tools are unchanged.Why was this change needed?
An agency customer driving Postiz from the MCP had no way to discover media already uploaded to the library: the public API only exposed upload routes and the MCP only
uploadFromUrlTool, so they copied URLs from the UI by hand.Other information:
typeandfileSizeare deliberately not exposed: upload never writes them, so every row keeps the defaults (typeis alwaysimage, even for videos, andfileSizeis 0 for almost every row).alt,thumbnailandthumbnailTimestampare deliberately not exposed either: they are the values last saved from some post's media settings dialog (each post keeps its own copy), so they do not describe the file.QA
bad-one.png), and create an API key for the organization.GET /public/v1/mediawithout an Authorization header. Expected: 401.GET /public/v1/mediawith the API key. Expected: 200 withpagesand 18results, newest first, each with exactlyid,name,originalName,pathandcreatedAt.GET /public/v1/media?page=2. Expected: the remaining rows, no overlap with page 1.GET /public/v1/media?page=abc,?page=0and?page=-3. Expected: 400 with a "page must not be less than 1" message each.GET /public/v1/media?search=bad-one, then?search=nomatchzzz. Expected: the matching row only (pages1), then{ "pages": 0, "results": [] }.processingand repeat the search for it. Expected: it is not listed; set it back toreadyand it is listed again.mediaListToolis present.mediaListToolwith no arguments, with{ "page": 2 }and with{ "search": "bad-one" }. Expected: the same rows as the REST calls, as{ pages, output }withcreatedAtas an ISO string.Checklist:
🤖 Generated with Claude Code